package fr.thomascoffin.mocaf;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Comparator;

/**
 * This class provides a simple way to make null-aware comparators, i.e. comparators that accept null values.
 * The developer only has to extends this class and implement the compareNotNull() method, that will always be given
 * not null parameters.
 * The NullableComparator will consider that a null value is less or greater than any not-null value depending
 * on the nullIsLessThanEverything value that is set at construction.
 *
 * @see NullableComparatorWrapper if you want to use this class with an existing Comparator.
 *      <p/>
 *      <p/>
 *      This class is released by Thomas Coffin (thomas.coffin@gmail.com) under the <a href="http://www.gnu.org/copyleft/lesser.html" target="_blank">LGPL License</a>
 *      as a component of the <a href="http://code.google.com/p/mocaf" target="_blank">mocaf project</a>
 *      <p/>
 *      (c) Thomas Coffin 2008.
 */
public abstract class NullableComparator<T> implements Comparator<T>
{
	private final boolean _nullIsLessThanEverything;

	/**
	 * Builds a NullableComparator precising if null values must be considered to be less or greater than any not null
	 * value.
	 *
	 * @param nullIsLessThanEverything true if null values must be considered to be less than any not null value, false
	 *                                 if they must be considered greater than any not null value.
	 */
	protected NullableComparator(boolean nullIsLessThanEverything)
	{
		_nullIsLessThanEverything = nullIsLessThanEverything;
	}

	/**
	 * Builds a NullabelComparator using the default behaviour, which considers than any null value is less
	 * than any not null value.
	 */
	protected NullableComparator()
	{
		this(true);
	}

	public final int compare(@Nullable T o1, @Nullable T o2)
	{
		if (o1 == null && o2 == null)
		{
			return 0;
		}
		else if (o1 == null)
		{
			return _nullIsLessThanEverything ? -1 : +1;
		}
		else if (o2 == null)
		{
			return _nullIsLessThanEverything ? +1 : -1;
		}
		else
		{
			return compareNotNull(o1, o2);
		}
	}

	/**
	 * The compare method that must be instanciated, and that will be used only with not null values.
	 *
	 * @param o1 the not-null o1 value, as in the standard compare(o1, o2) method.
	 * @param o2 the not-null o2 value, as in the standard compare(o1, o2) method.
	 * @return 0 if o1 and o2 are to be considered equal, a negative value if o1 is less than o2, a positive value if
	 *         o1 is greater than o2.
	 */
	protected abstract int compareNotNull(@NotNull T o1, @NotNull T o2);

	private static boolean equals(@Nullable Object o1, @Nullable Object o2, boolean checkBothEqualities)
	{
		if (null == o1)
		{
			// Both are null, and so are equal to each other
			return null == o2;
		}
		else if (o2 == null)
		{
			// If o1 is not null, o2 must be not null
			return false;
		}
		else
		{
			return o1.equals(o2) && (!checkBothEqualities || o2.equals(o1));
		}
	}

	/**
	 * Checks if o1.equals(o2), but works also with null values on both sides,
	 * considering that null is equal to null, and that any non null value is not equal to null.
	 *
	 * @param o1 the first value to check, may be null.
	 * @param o2 the second value to check, may be null.
	 * @return true if o1 and o2 are equal
	 */
	public static boolean equals(@Nullable Object o1, @Nullable Object o2)
	{
		return equals(o1, o2, false);
	}

	/**
	 * As <code>equals()</code>, checks if o1 and o2 are equal to each other, working with null values as well,
	 * but checks not only if o1.equals(o2), but also if o2.equals(o1), to deal with possibly inconsistent equal() methods
	 * in o1 and o2.
	 *
	 * @param o1 the first value to check, may be null.
	 * @param o2 the second value to check, may be null.
	 * @return true if o1 and o2 are equal
	 */
	public static boolean bothEquals(@Nullable Object o1, @Nullable Object o2)
	{
		return equals(o1, o2, true);
	}
}
